TypeScriptのDIコンテナライブラリ InversifyJSとTsyringeの基本的な使い方を比較してみた
リテールアプリ共創部のるおんです。先日 TypeScript を用いたバックエンドの初期開発においてDIコンテナを実装する際にどのライブラリを使用するかを検討する機会がありました。その際、TypeScriptでDIを実現するためのライブラリのうち、よく使われているライブラリである、Tsyringe と InversifyJS の基本的な使い方を比較したので共有したいと思います。
本記事で紹介すること/紹介しないこと
本記事では同じ機能を実現するコードを、TsyringeとInversifyJSのそれぞれのコードを比較しながら解説します。
DI(依存性注入)の基本的な考え方や、DIコンテナとはなどは解説しません。
DIについてはこちらの記事が大変参考になりました。今回はこのブログで紹介されているTsyrigneのコードを、InversifyJSで書いた場合と比較してみたいと思います。
DIコンテナライブラリ
まずそれぞれのライブラリについて
Tsyringe
Tsyringeは、Microsoftが開発したDIコンテナライブラリです。
公式によると、
A lightweight dependency injection container for TypeScript/JavaScript for constructor injection.
とのことで、InversifyJSに比べ、よりシンプルで軽量なDIコンテナライブラリです。デコレータを使用して依存関係を定義し、自動的に依存関係を解決します。
InversifyJS
InversifyJSは、より高度な機能を持つDIコンテナライブラリです。
公式によると、
A powerful and lightweight inversion of control container
for JavaScript & Node.js apps powered by TypeScript.
とのことで、 パワフルかつ軽量なDIコンテナライブラリです。
TypeScriptのDIコンテナライブラリについて検索すると、このライブラリがよく使用されている印象です。また、Githubのスター数や使用率もこちらの方が多いです。
Tsyringeと同様にデコレータを使用しますが、より細かい設定が可能かつ多機能で、大規模なアプリケーションにも適しています。
使用率の比較
2024年8月25日時点では、InversifyJSはTsyringeのおよそ2倍ほどダウンロードされており、GitHubのstar数も2倍以上になっています。
また、InversifyJSは公式ドキュメントが用意されていますが、TsyringeはGithubリポジトリのREADMEがその役割を果たしています。
基本的な使用方法の比較
それでは、両ライブラリの基本的な使い方を比較してみましょう。
繰り返しになりますが、本記事ではライブラリのコード比較に徹し、DIの解説や必要性などは解説いたしません。
先ほど紹介したこちらの記事が個人的には大変参考になりました。こちらの記事ではTsyringeの書き方についてのみ記述されていたので、そのコードをお借りして、InversifyJSでも同様に書いてみて比較していきたいと思います。
以下の3つのファイルを中心に見ていきます。
- database.ts
- user.ts
- index.ts
セットアップ
まずはライブラリのインストールです。DIコンテナを実装するには、両ライブラリでreflect-metadataをインストールする必要があります。これにより関数が持つメタ情報を扱えるようになります。
npm install reflect-metadata
次に各ライブラリをinstallします。
// inversifyの場合
npm install inversify
// Tsyringeの場合
npm install tsyringe
次にtsconfig.json
を修正してください。これにより、デコレーター機能と、メタデータ取得機能が使えるようになります。
{
'compilerOptions': {
'target': 'ES5',
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
}
user.ts
まずは、依存関係を受け取るクラスであるUserクラスを定義します。UserクラスにはIDatabaseインターフェースを実装したオブジェクトが依存注入されます。
以下に各ライブラリで書いてみます。
Tsyringe:
import { inject, injectable } from "tsyringe";
export interface IDatabase {
saveUser: (user: User) => void;
}
@injectable()
export default class User {
userId: number = 0
userName: string = "taro"
constructor(
@inject("IDatabase") private database: IDatabase) { }
saveUser() {
if (this.userId) {
this.database.saveUser(this)
}
}
}
InversifyJS:
export const TYPES = {
IDatabase: Symbol.for("IDatabase"),
User: Symbol.for("User")
}
import { inject, injectable } from "inversify";
import { TYPES } from "./types"
export interface IDatabase {
saveUser: (user: User) => void;
}
@injectable()
export default class User {
userId: number = 0
userName: string = "taro"
constructor(
@inject(TYPES.IDatabase) private database: IDatabase) { }
saveUser() {
if (this.userId) {
this.database.saveUser(this)
}
}
}
はい、user.tsファイルに関してはほとんど同じです。両方とも依存注入可能なクラスには@injectable()
デコレーターを使用し、@inject
でinterfaceへの依存を注入しています。
Tsyringeでは、依存関係を文字列で指定します(@inject("IDatabase"))。
InversifyJSに関しては、@inject
の引数にtypesファイルで別途定義した依存関係の識別子を使用するようにしています。(@inject(TYPES.IDatabase))
これは、公式ドキュメントによると、
PLEASE MAKE SURE TO PLACE THIS TYPES DECLARATION IN A SEPARATE FILE. (see bug #1455)
(省略)
Note: It is recommended to use Symbols but InversifyJS also support the usage of Classes and string literals
というようにtypesファイルを別に使用するよう強調されています。
また、その識別子にはクラスや文字列でも可能ですが、Symboleを使用することが推奨されています。
Tsyringeの実用においても、識別子のファイルを別途設けるのもありだと思います。
database.ts
次に、Userクラスに注入される具体的な実装であるDatabaseクラスを定義します。このクラスはIDatabaseインターフェースを実装しています。
Tsyringe:
import User, { IDatabase } from "./user";
export default class Database implements IDatabase {
saveUser(user: User) {
console.log(`Saved ${user.userName}! with tsyringe`);
}
}
InversifyJS:
import { injectable } from "inversify";
import User, { IDatabase } from "./user";
@injectable()
export default class Database implements IDatabase {
saveUser(user: User) {
console.log(`Saved ${user.userName}! with InversifyJS`);
}
}
こちらもほとんど同じですが、
InversifyJSでは、Userクラスに注入される実装クラスであるDatabaseクラスにも@injectable()デコレータを使用して注入可能にマークする必要があります。 Tsyringeでは、この例ではデコレータは不要です。
依存関係の解決
最後に、DIコンテナの設定と使用方法を見てみましょう。
Tsyringe:
import 'reflect-metadata'
import { container } from "tsyringe";
import Database from "./database";
import User from "./user";
container.register("IDatabase", { useClass: Database });
export const user = container.resolve(User)
user.userId = 100
user.userName = "hanako"
user.saveUser()
// => Saved hanako! with tsyringe
InversifyJS:
import { Container } from "inversify";
import "reflect-metadata";
import Database from "./database";
import { TYPES } from "./types";
import User from "./user";
const container = new Container()
container.bind<Database>(TYPES.IDatabase).to(Database)
container.bind<User>(TYPES.User).to(User)
const user = container.get<User>(TYPES.User)
user.userId = 100
user.userName = "hanako"
user.saveUser()
// => Saved hanako! with inversify
InversifyJSではContainerクラスを初期化する必要があります。また、公式によるとinversify.config.ts
というファイル名のファイルにおいてコンテナの作成と設定を行うことが推奨されています。Tsyringeは適当にdiContainer.ts
として作成しました。
主な違いについて、
依存関係の登録:
- Tsyringeでは、container.register()を使用して依存関係を登録します。
- InversifyJSでは、container.bind().to()で登録します。
インスタンスの取得:
- Tsyringeでは、container.resolve()でインスタンスを取得します。
- InversifyJSでは、container.get()でインスタンスを取得します。。
登録の範囲:
- Tsyringeでは、@injectable()デコレータが付いたクラス(この場合はUser)は自動的にDIコンテナに登録されるため、Userクラスの明示的な登録が不要です。インターフェース(IDatabase)と具体的な実装(Database)の紐付けのみを行っています。
- InversifyJSでは、すべての依存関係(DatabaseとUser)を明示的に登録する必要があります。そのため、Userクラスもbindして登録する必要があります。
おわりに
どうでしたでしょうか。今回はTsyringeとInversifyJSの簡単なコードを見てみて、記述方法の違いを比較してみました。実際に書いてみてTsyringeの方が記述するコードや必要なファイルがInversifyJSに比べ少なく、直感的に感じました。一方で、InversifyJSの方がコード量が多い分より多機能で、大規模なプロジェクトや複雑な依存関係を持つアプリケーションに適しているようです。
今回二つのライブラリを比較することで、それぞれのライブラリの使い方を理解することができました。
参考になれば幸いです。